﻿@using System.Collections.Specialized
@using System.Text.RegularExpressions
@using StackExchange.Exceptional
@using StackExchange.Opserver
@using StackExchange.Opserver.Controllers
@model StackExchange.Opserver.Views.Exceptions.ExceptionsModel
@{
    var error = Model.Exception;
    const string customDataErrorKey = "CustomDataFetchError";
    this.SetPageTitle("Error Info - " + error);
}
@section head {
    @if (error != null)
    {
        var sb = StringBuilderCache.Get();
        error.WriteDetailedJson(sb);
        var json = sb.ToStringRecycle();
        <script>
            var Exception = @json.AsHtml();
            $(function () {
                Status.Exceptions.init({ store: @Model.Store.Name.ToJson(), group: @(Model.Group?.Name.ToJson()), log: @error.ApplicationName.ToJson(), id: '@error.GUID.ToString()' });
            });
        </script>
    }
}
@functions
{
    private static readonly HashSet<string> HiddenHttpKeys = new HashSet<string>
{
    "ALL_HTTP",
    "ALL_RAW",
    "HTTP_CONTENT_LENGTH",
    "HTTP_CONTENT_TYPE",
    "HTTP_COOKIE",
    "QUERY_STRING"
};

    private static readonly HashSet<string> DefaultHttpKeys = new HashSet<string>
{
    "APPL_MD_PATH",
    "APPL_PHYSICAL_PATH",
    "GATEWAY_INTERFACE",
    "HTTP_ACCEPT",
    "HTTP_ACCEPT_CHARSET",
    "HTTP_ACCEPT_ENCODING",
    "HTTP_ACCEPT_LANGUAGE",
    "HTTP_CONNECTION",
    "HTTP_KEEP_ALIVE",
    "HTTPS",
    "INSTANCE_ID",
    "INSTANCE_META_PATH",
    "PATH_INFO",
    "PATH_TRANSLATED",
    "REMOTE_PORT",
    "SCRIPT_NAME",
    "SERVER_NAME",
    "SERVER_PORT",
    "SERVER_PORT_SECURE",
    "SERVER_PROTOCOL",
    "SERVER_SOFTWARE"
};

    private static readonly Regex _sanitizeUrl = new Regex(@"[^-a-z0-9+&@#/%?=~_|!:,.;\*\(\)\{\}]", RegexOptions.IgnoreCase | RegexOptions.Compiled);
    public static string SanitizeUrl(string url)
    {
        return url.IsNullOrEmpty() ? url : _sanitizeUrl.Replace(url, "");
    }
}

@helper Linkify(string possibleLink)
    {
        if (possibleLink.IsNullOrEmpty())
        {
            @possibleLink
        }
        if (possibleLink != null)
        {
            if (Regex.IsMatch(possibleLink, @"%[A-Z0-9][A-Z0-9]"))
            {
                possibleLink = Server.UrlDecode(possibleLink);
            }
            if (Regex.IsMatch(possibleLink, "^(https?|ftp|file)://"))
            {
                var sane = SanitizeUrl(possibleLink);
                if (sane == possibleLink) // only link if it's not suspicious
                {
                    <a href="@sane">@possibleLink</a>
                    return;
                }
            }
        }
        @possibleLink
}
@helper RenderVariableTable(string title, string className, NameValueCollection vars)
    {
        if (vars == null || vars.Count == 0)
        {
            return;
        }
        // If this is a hidden row, buffer it up, since CSS has no clean mechanism for :visible:nth-row(odd) type styling behavior
        Func<string, bool> isHidden = k => DefaultHttpKeys.Contains(k);
        var allKeys = vars.AllKeys.Where(key => !HiddenHttpKeys.Contains(key) && vars[key].HasValue()).OrderBy(k => k);

        <div class="@className">
            <h5><strong>@title</strong></h5>
            <div class="side-scroll">
                <table class="table table-striped table-hover table-super-condensed table-responsive">
                    <tbody>
                        @foreach (var k in allKeys.Where(k => !isHidden(k)))
                        {
                            <tr>
                                <td class="text-nowrap">@k</td>
                                <td>@Linkify(vars[k])</td>
                            </tr>
                        }
                        @if (vars["HTTP_HOST"].HasValue() && vars["URL"].HasValue())
                        {
                            var ssl = vars["HTTP_X_FORWARDED_PROTO"] == "https" || vars["HTTP_X_SSL"].HasValue() || vars["HTTPS"] == "on";
                            var url = $"http{(ssl ? "s" : "")}://{vars["HTTP_HOST"]}{vars["URL"]}{(vars["QUERY_STRING"].HasValue() ? "?" + vars["QUERY_STRING"] : "")}";
                            <tr>
                                <td class="text-nowrap">URL and Query</td>
                                <td>
                                    @if (vars["REQUEST_METHOD"] == "GET")
                                    {
                                        @Linkify(url)
                                    }
                                    else
                                    {
                                        @url.HtmlEncode()
                                    }
                                </td>
                            </tr>
                        }
                        @foreach (var k in allKeys.Where(k => isHidden(k)))
                        {
                            <tr class="hidden">
                                <td>@k</td>
                                <td>@Linkify(vars[k])</td>
                            </tr>
                        }
                    </tbody>
                </table>
            </div>
        </div>
}
@helper RenderKeyValueTable(IEnumerable<KeyValuePair<string, string>> kvPairs)
    {
        if (kvPairs?.Any() == true)
        {
            <div class="side-scroll">
                <table class="alt-rows key-value">
                    @foreach (var kv in kvPairs)
                    {
                        <tr>
                            <td>@kv.Key</td>
                            <td>@Linkify(kv.Value)</td>
                        </tr>
                    }
                </table>
            </div>
        }
}

<div id="ErrorInfo" class="js-exception">
    @if (error == null)
    {
        <h5 class="page-header">
            <a href="@Url.Action(nameof(ExceptionsController.Exceptions))">Exceptions</a>
            <span class="text-muted">/</span>
            Unknown
        </h5>
        <div class="alert alert-warning">
            <h4>Error was not found.</h4>
            If this link worked previously, the error was hard deleted.
        </div>
    }
    else
    {
        <h5 class="page-header">
            <a href="@Url.Action(nameof(ExceptionsController.Exceptions), new { store = Model.Store.Name })">@Model.Store.Name Exceptions</a>
            <span class="text-muted">/</span>
            <a href="@Url.Action(nameof(ExceptionsController.Exceptions), new { store = Model.Store.Name, log = error.ApplicationName })">@error.ApplicationName</a>
            <span class="text-muted">/</span>
            @error.Type
            <span class="pull-right"><a href="@Url.Action(nameof(ExceptionsController.Similar), new { id = error.GUID, log = error.ApplicationName, store = Model.Store?.Name })">View Similar</a></span>
        </h5>
        <p class="small text-danger js-error-message">@error.Message</p>
        <div class="error-info">
            <pre class="stack dark"><code class="nohighlight">@ExceptionalUtils.StackTrace.HtmlPrettify(error.Detail, Current.Settings.Exceptions.StackTraceSettings).AsHtml()</code></pre>
            <p class="small">
                occurred <b title="@error.CreationDate.ToLongDateString() at @error.CreationDate.ToLongTimeString()">@error.CreationDate.ToRelativeTime()</b> (@error.CreationDate.ToZuluTime()) on @error.MachineName
                @if (!error.DeletionDate.HasValue && Current.User.IsExceptionAdmin)
                {
                    <span class="text-muted pipe-delim js-exception-actions">(<a href="#" data-url="@Url.Action(nameof(ExceptionsController.Delete))" title="delete this error from the log"><i class="fa fa-times fa-fw text-danger" aria-hidden="true"></i>delete</a>
                        @if (!error.IsProtected) {
                            <span class="text-muted">|</span>
                            <a href="#" data-url="@Url.Action(nameof(ExceptionsController.Protect))" title="protect this error"><i class="fa fa-lock fa-fw text-warning" aria-hidden="true"></i>protect</a>
                        }
                     )</span>
                }
            </p>
            @if ((Current.Settings.Jira?.Enabled ?? false) && Current.User.IsExceptionAdmin)
            {
                @Html.Action("JiraActions", new { appName = error.ApplicationName })
            }
            @if (error.Commands != null)
            {
                foreach (var cmd in error.Commands)
                {
                    var lang = cmd.GetHighlightLanguage();
                    <h4>Command: @cmd.Type</h4>
                    <pre class="command"><code class="@(cmd.Type.Contains("SQL") ? "sql" : (lang.HasValue() ? lang : null))">@cmd.CommandString</code></pre>

                    RenderKeyValueTable(cmd.Data);
                }
            }
            @RenderVariableTable("Server Variables", "server-variables", error.ServerVariables)
            @if (error.CustomData?.Count > 0)
            {
                var errored = error.CustomData.ContainsKey(customDataErrorKey);
                var cdKeys = error.CustomData.Keys.Where(k => k != customDataErrorKey).ToList();
                <div class="custom-data">
                    @if (errored)
                    {
                        <h5 class="text-danger"><strong>Custom - Error while gathering custom data</strong></h5>
                        <span>GetCustomData threw an exception:</span>
                        <pre class="error-detail">@error.CustomData[customDataErrorKey]</pre>
                    }
                    else if (cdKeys.Any())
                    {
                        <h5><strong>Custom</strong></h5>
                        <div class="side-scroll">
                            <table class="table table-striped table-hover table-super-condensed table-responsive">
                                <tbody>
                                    @foreach (var cd in cdKeys)
                                    {
                                        <tr>
                                            <td class="text-nowrap">@cd</td>
                                            <td>@Linkify(error.CustomData[cd])</td>
                                        </tr>
                                    }
                                </tbody>
                            </table>
                        </div>
                    }
                </div>
            }
            @RenderVariableTable("Query Parameters", "querystring", error.QueryString)
            @RenderVariableTable("Form", "form", error.Form)
            @RenderVariableTable("Cookies", "cookies", error.Cookies)
            @RenderVariableTable("Request Headers", "headers", error.RequestHeaders)
        </div>
    }
</div>
